package edu.uprm.ece.icom4035.polynomial;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.StringTokenizer;

public class PolynomialImp implements Polynomial {

	private ArrayList<Term> terms;
	private static int INITIALCAP = 0;

	/**
	 * Creates an empty polynomial
	 */
	public PolynomialImp() {
		terms = new ArrayList<Term>(INITIALCAP);
	}

	/**
	 * Creates a polynomial from a given string
	 * @param pol input polynomial
	 * (Pre: Polynomial must be in order)
	 */
	public PolynomialImp(String pol) {
		terms = new ArrayList<Term>(INITIALCAP);
		this.fromString(pol);
	}

	/**
	 * Creates a polynomial from an array of terms
	 * @param terms input array of terms
	 */
	private PolynomialImp(ArrayList<Term> terms) {
		this.terms = new ArrayList<Term>(INITIALCAP);
		for(int i=0; i<terms.size(); i++) {
			this.addTerm(terms.get(i));
		}
	}

	/**
	 * Creates a polynomial of a single term
	 * @param term single input term
	 */
	private PolynomialImp(TermImp term) {
		this.terms = new ArrayList<Term>(INITIALCAP);
		this.terms.add(term);
	}

	/**
	 * Makes an iterator for the terms in the polynomial
	 * @return term iterator
	 */
	public Iterator<Term> iterator() {
		return this.terms.listIterator();
	}

	/**
	 * Adds two polynomials together
	 * @param P2 polynomial to be added
	 * @return the sum of polynomials
	 */
	public Polynomial add(Polynomial P2) {
		Iterator<Term> itr = P2.iterator();
		ArrayList<Term> elements = new ArrayList<Term>(INITIALCAP);
		while(itr.hasNext()) {
			elements.add((Term) itr.next());
		}
		ArrayList<Term> newTerms = new ArrayList<Term>(INITIALCAP);
		int counter1 = 0;
		int counter2 = 0;
		double newCoeff = 0;
		while(counter1 < terms.size() || counter2 < elements.size()) {
			if (counter1 == terms.size() && counter2 < elements.size()) {
				newCoeff = elements.get(counter2).getCoefficient();
				newTerms.add(new TermImp(newCoeff, elements.get(counter2).getExponent()));
				counter2++;
			}
			else if (counter2 == elements.size() && counter1 < terms.size()) {
				newCoeff = terms.get(counter1).getCoefficient();
				newTerms.add(new TermImp(newCoeff, terms.get(counter1).getExponent()));
				counter1++;
			}
			else if (terms.get(counter1).getExponent() > elements.get(counter2).getExponent()) {
				newCoeff = terms.get(counter1).getCoefficient();
				newTerms.add(new TermImp(newCoeff, terms.get(counter1).getExponent()));
				counter1++;
			}
			else if (terms.get(counter1).getExponent() < elements.get(counter2).getExponent()) {
				newCoeff = elements.get(counter2).getCoefficient();
				newTerms.add(new TermImp(newCoeff, elements.get(counter2).getExponent()));
				counter2++;
			}
			else if (terms.get(counter1).getExponent() == elements.get(counter2).getExponent()) {
				newCoeff = terms.get(counter1).getCoefficient() + elements.get(counter2).getCoefficient();
				newTerms.add(new TermImp(newCoeff, terms.get(counter1).getExponent()));
				counter1++;
				counter2++;
			}
		}
		return new PolynomialImp(newTerms);
	}

	/**
	 * Subtract two polynomials
	 * @param P2 polynomial to be subtracted
	 * @return difference of the polynomials
	 */
	public Polynomial subtract(Polynomial P2) {
		Polynomial P3;
		if (this.equals(P2)) {
			P3 = new PolynomialImp();
		}
		else {
			P2.multiply(-1);
			P3 = this.add(P2);
		}
		return P3;
	}

	/**
	 * Multiplies two polynomials
	 * @param P2 polynomial to be multiplied
	 * @return product of both polynomials
	 */
	public Polynomial multiply(Polynomial P2) {
		Iterator<Term> itr = P2.iterator();
		ArrayList<Term> elements = new ArrayList<Term>(INITIALCAP);
		Polynomial Pfinal = new PolynomialImp();
		while(itr.hasNext()) {
			elements.add((Term) itr.next());
		}
		double newCoeff = 0;
		int newExp = 0;
		for(int i=0; i<elements.size(); i++) {
			for(int j=0; j<terms.size(); j++) {
				newCoeff = terms.get(j).getCoefficient()*elements.get(i).getCoefficient();
				newExp = terms.get(j).getExponent() + elements.get(i).getExponent();
				if (i==0 && j==0) {
					Pfinal = new PolynomialImp(new TermImp(newCoeff, newExp));
				}
				else {
					Pfinal = Pfinal.add(new PolynomialImp(new TermImp(newCoeff, newExp)));
				}
			}
		}
		return Pfinal;
	}

	/**
	 * Multiplies polynomial by a value
	 * @param c value to be multiplied to polynomial
	 * @return result of polynomial multiplied by the value c
	 */
	public Polynomial multiply(double c) {
		for(int i=0; i<terms.size(); i++) {
			terms.set(i, (new TermImp(terms.get(i).getCoefficient()*c, terms.get(i).getExponent())));
		}

		return new PolynomialImp(terms);
	}

	/**
	 * Calculates the derivative of the polynomial
	 * @return derivative of the polynomial
	 */
	public Polynomial derivative() {
		ArrayList<Term> temp = new ArrayList<Term>(INITIALCAP);
		double newCoeff;
		int newExpo;
		for (int i=0; i<this.terms.size(); i++) {
			newCoeff = terms.get(i).getCoefficient() * terms.get(i).getExponent();
			newExpo = terms.get(i).getExponent() - 1;
			temp.add(new TermImp(newCoeff, newExpo));
		}
		return new PolynomialImp(temp);
	}

	/**
	 * Calculates the indefinite integral of the polynomial
	 * @return indefinite integral of the polynomial
	 */
	public Polynomial indefiniteIntegral() {
		ArrayList<Term> temp = new ArrayList<Term>();
		double newCoeff;
		int newExpo;
		for (int i=0; i<this.terms.size(); i++) {
			newExpo = terms.get(i).getExponent() + 1;
			newCoeff = terms.get(i).getCoefficient() / newExpo;
			temp.add(new TermImp(newCoeff, newExpo));
		}
		temp.add(new TermImp(1.0, 0));
		return new PolynomialImp(temp);
	}

	/**
	 * Calculates the definite integral of a polynomial between two points
	 * @param a initial point
	 * @param b final point
	 * @return definite integral of the polynomial
	 */
	public double definiteIntegral(double a, double b) {
		double result = 0;
		Polynomial P1 = this.indefiniteIntegral();
		result = P1.evaluate(b) - P1.evaluate(a);
		return result;
	}

	/**
	 * Gets the highest degree of the polynomial
	 * @return highest degree of the polynomial
	 */
	public int degree() {
		int maxDeg = 0;
		for (int i = 0; i < terms.size(); i++) {
			if (maxDeg < terms.get(i).getExponent()) {
				maxDeg = terms.get(i).getExponent();
			}
		}
		return maxDeg;
	}

	/**
	 * Evaluates the polynomial with the given value
	 * @param x value to evaluate the polynomial
	 * @return value of the evaluation of the polynomial
	 */
	public double evaluate(double x) {
		double result = 0;
		for (int i=0; i<terms.size(); i++) {
			result = result + terms.get(i).getCoefficient()*Math.pow(x, terms.get(i).getExponent());
		}
		return result;
	}

	/**
	 * Returns a string representation of the polynomial
	 * @return string representation of the polynomial
	 */
	public String toString() {
		String pol = "";
		for (int i=0; i<terms.size(); i++) {
			if (terms.get(i).getCoefficient() !=0) {
				if (i == 0) {
					pol = pol + terms.get(i).toString();					
				}
				else{
					pol = pol + "+" + terms.get(i).toString();
				}
			}	
		}	
		return pol;
	}

	/**
	 * Checks if both polynomials are the same
	 * @param P polynomial to check equality
	 * @return true or false
	 */
	public boolean equals(Polynomial P2) {
		return this.toString().equals(P2.toString());
	}

	/**
	 * Converts a string into a polynomial
	 * @param str input string
	 */
	private void fromString(String str) {
		StringTokenizer strTok = new StringTokenizer(str, "+");
		String nextStr = null;
		Term nextTerm = null;
		this.terms.clear();
		while (strTok.hasMoreElements()){
			nextStr = (String) strTok.nextElement();
			nextTerm = TermImp.fromString(nextStr);
			// private method to store a new term into a polynomial
			this.addTerm(nextTerm);
		}

	}

	/**
	 * Add a new term to the array of terms
	 * @param nextTerm new term to be added
	 */
	private void addTerm(Term nextTerm) {
		terms.add(nextTerm);
	}
}
